package com.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shortlink.admin.base.ShortUriUtils;
import com.shortlink.admin.config.AdminConfiguration;
import com.shortlink.admin.dao.entity.DomainDO;
import com.shortlink.admin.dao.entity.ShortLinkDO;
import com.shortlink.admin.dao.mapper.ShortLinkMapper;
import com.shortlink.admin.dto.req.shortlink.ShortLinkBatchCreateReqDTO;
import com.shortlink.admin.dto.req.shortlink.ShortLinkCreateReqDTO;
import com.shortlink.admin.dto.req.shortlink.ShortLinkGroupPageReqDTO;
import com.shortlink.admin.dto.req.shortlink.ShortLinkUpdateReqDTO;
import com.shortlink.admin.dto.resp.shortlink.ShortLinkVO;
import com.shortlink.admin.dto.transform.GroupShortLinkCountDTO;
import com.shortlink.admin.remote.ShortLinkMapperRemoteService;
import com.shortlink.admin.remote.ShortLinkStatsRemoteService;
import com.shortlink.admin.remote.dto.shortlinkmapper.ShortLinkMapperInsertReqDTO;
import com.shortlink.admin.remote.dto.shortlinkmapper.ShortLinkMapperUpdateReqDTO;
import com.shortlink.admin.remote.dto.shortlinkmapper.ShortLinkReqDTO;
import com.shortlink.admin.remote.dto.shortlinkstats.ShortLinkTodayBaseStatsRespDTO;
import com.shortlink.admin.service.DomainService;
import com.shortlink.admin.service.GroupService;
import com.shortlink.admin.service.ShortLinkService;
import com.shortlink.common.base.Result;
import com.shortlink.common.exception.ClientException;
import com.shortlink.common.exception.RemoteException;
import com.shortlink.common.exception.ServiceException;
import com.shortlink.common.user.UserContext;
import com.shortlink.common.util.RBloomFilterUtils;
import com.shortlink.common.util.UrlUtils;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.shortlink.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 * 循环依赖问题：https://springdoc.cn/revisiting-spring-s-circular-dependencies/
 *
 * @author wp
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final AdminConfiguration adminConfiguration;
    private final ShortLinkMapperRemoteService shortLinkMapperRemoteService;
    private final ShortLinkStatsRemoteService shortLinkStatsRemoteService;
    private final RBloomFilterUtils bloomFilterUtils;
    private final DomainService domainService;
    @Lazy
    @Resource
    private GroupService groupService;
    @Lazy
    @Resource
    private ShortUriUtils shortUriUtils;

    @Override
    public List<ShortLinkReqDTO> queryShortLinkByGid(Long gid) {
        return lambdaQuery()
                .eq(ShortLinkDO::getUserId, UserContext.getUserId())
                .eq(ShortLinkDO::getGid, gid)
                .eq(ShortLinkDO::getDelFlag, ShortLinkDO.DEL_FLAG_FAIL)
                .list()
                .stream()
                .map(shortLink -> new ShortLinkReqDTO(shortLink.getDomain(), shortLink.getShortUri()))
                .toList();
    }

    @Override
    public IPage<ShortLinkVO> pageGroup(ShortLinkGroupPageReqDTO shortLinkPage) {
        String gid = shortLinkPage.getGid();
        if (ObjectUtil.isEmpty(gid)) {
            log.info(String.format("gid(%s) 不能为空", gid));
            throw new ClientException("参数错误");
        }
        shortLinkPage.verify();

        Page<ShortLinkDO> page = new Page<>(shortLinkPage.getCurrent(), shortLinkPage.getSize());
        QueryWrapper<ShortLinkDO> wrapper = new QueryWrapper<>();
        wrapper = wrapper
                .eq(ShortLinkDO.USER_ID_NAME, UserContext.getUserId())
                .eq(ShortLinkDO.GID_NAME, gid)
                .eq(ShortLinkDO.DEL_FLAG_NAME, 0);
        return queryPage(page, wrapper, shortLinkPage.getOrderTag(), shortLinkPage.getDesc());
    }

    @Override
    public IPage<ShortLinkVO> queryPage(Page<ShortLinkDO> page, QueryWrapper<ShortLinkDO> queryWrapper, String orderTag, Boolean desc) {
        if (orderTag != null) {
            if (!Boolean.FALSE.equals(desc)) {
                queryWrapper = queryWrapper.orderByDesc(orderTag);
            } else {
                queryWrapper = queryWrapper.orderByAsc(orderTag);
            }
        }

        Page<ShortLinkDO> shortLinkPage = page(page, queryWrapper);
        List<ShortLinkReqDTO> shortLinkReqs = shortLinkPage.getRecords().stream()
                .map(shortLink -> new ShortLinkReqDTO(shortLink.getDomain(), shortLink.getShortUri()))
                .toList();
        Result<List<ShortLinkTodayBaseStatsRespDTO>> todayBaseStatsResult = shortLinkStatsRemoteService.batchGetTodayShortLinkBaseStats(shortLinkReqs);
        if (!todayBaseStatsResult.isSuccess()) {
            throw new RemoteException(todayBaseStatsResult.getMessage());
        }

        List<ShortLinkTodayBaseStatsRespDTO> todayBaseStatsList = todayBaseStatsResult.getData();
        List<ShortLinkVO> shortLinkVOList = new ArrayList<>();
        for (int i = 0; i < todayBaseStatsList.size(); i++) {
            ShortLinkDO shortLink = shortLinkPage.getRecords().get(i);
            ShortLinkVO shortLinkVO = BeanUtil.copyProperties(shortLink, ShortLinkVO.class);
            String fullShortUrl = UrlUtils.concatShortUrl(shortLink.getDomain(), shortLink.getShortUri());
            shortLinkVO.setFullShortUrl(fullShortUrl);
            ShortLinkTodayBaseStatsRespDTO todayBaseStats = todayBaseStatsList.get(i);
            shortLinkVO.setTodayPv(todayBaseStats.getPv());
            shortLinkVO.setTodayUv(todayBaseStats.getUv());
            shortLinkVO.setTodayIp(todayBaseStats.getIp());
            shortLinkVO.setTotalPv(todayBaseStats.getNewPv() + shortLink.getTotalPv());
            shortLinkVO.setTotalUv(todayBaseStats.getNewUv() + shortLink.getTotalUv());
            shortLinkVO.setTotalIp(todayBaseStats.getNewIp() + shortLink.getTotalIp());
            shortLinkVOList.add(shortLinkVO);
        }
        Page<ShortLinkVO> shortLinkVOPage = new Page<>();
        BeanUtil.copyProperties(shortLinkPage, shortLinkVOPage, "records");
        shortLinkVOPage.setRecords(shortLinkVOList);
        return shortLinkVOPage;
    }

    @Override
    @Transactional
    public void createShortLink(ShortLinkCreateReqDTO shortLinkCreate) {
        Long gid = shortLinkCreate.getGid();
        String description = shortLinkCreate.getDescription();
        String domain = shortLinkCreate.getDomain();
        // todo
        if (domain == null) {
            domain = adminConfiguration.getDefaultDomain();
        }
        String originUrl = shortLinkCreate.getOriginUrl();
        LocalDate validDate = shortLinkCreate.getValidDate();
        if (ObjectUtil.isAllNotEmpty(domain, gid, description)) {
            log.info(String.format("gid(%s) domain(%s) description(%s) 不能为空", gid, domain, description));
            throw new ClientException("参数错误");
        }
        String groupName = groupService.getGroupNameById(gid);
        if (ObjectUtil.isEmpty(groupName)) {
            log.info("分组不存在");
            throw new ClientException("参数错误");
        }
        DomainDO domainDO = verifyDomain(domain);
        verifyUrl(originUrl);

        String shortUri = shortUriUtils.generate(domainDO, originUrl);
        long id = redissonClient.getIdGenerator(SHORT_LINK_ID_GENERATOR_KEY).nextId();
        ShortLinkDO shortLink = ShortLinkDO.builder()
                .id(id)
                .userId(UserContext.getUserId())
                .domain(domain)
                .originUrl(originUrl)
                .gid(gid)
                .groupName(groupName)
                .validDate(validDate)
                .description(description)
                .shortUri(shortUri)
                .favicon(UrlUtils.getFavicon(originUrl))
                .build();
        boolean succeed = save(shortLink);
        if (!succeed) {
            throw new ServiceException("短链接插入失败");
        }

        ShortLinkMapperInsertReqDTO shortLinkMapperInsert =
                new ShortLinkMapperInsertReqDTO(id, domain, shortUri, originUrl, shortLinkCreate.getValidDate());
        Result<Void> result = shortLinkMapperRemoteService.insert(shortLinkMapperInsert);
        succeed = result.isSuccess();
        if (!succeed) {
            throw new RemoteException(result.getMessage());
        }

        RBloomFilter<Object> shortUriBloomFilter = bloomFilterUtils.create(DOMAIN_SHORT_URI_BLOOM_KEY);
        shortUriBloomFilter.add(shortUri);
        String redisKey = String.format(SHORT_LINK_MAPPER_KEY, domain, shortUri);
        stringRedisTemplate.opsForValue().set(redisKey, originUrl, SHORT_LINK_MAPPER_CREATE_TTL, TimeUnit.MINUTES);
    }

    @Transactional
    @Override
    public void batchCreateShortLink(ShortLinkBatchCreateReqDTO shortLinkBatchCreate) {
        //参数校验
        Long gid = shortLinkBatchCreate.getGid();
        final String domain = Optional
                .ofNullable(shortLinkBatchCreate.getDomain())
                .orElse(adminConfiguration.getDefaultDomain());
        // todo
        List<String> originUrls = shortLinkBatchCreate.getOriginUrls();
        List<String> descriptions = shortLinkBatchCreate.getDescriptions();
        LocalDate validDate = shortLinkBatchCreate.getValidDate();
        if (!ObjectUtil.isAllNotEmpty(gid, originUrls, descriptions)) {
            String msg = "gid{%s} domain{%s} originUrls{%s} descriptions{%s} 不能为空";
            log.info(String.format(msg, gid, domain, originUrls, descriptions));
            throw new ClientException("参数错误");
        }
        if (originUrls.size() != descriptions.size()) {
            String msg = "originUrls.size{%s} descriptions.size{%s} 长度不一致";
            log.info(String.format(msg, originUrls.size(), descriptions.size()));
            throw new ClientException("参数错误");
        }
        String groupName = groupService.getGroupNameById(gid);
        if (ObjectUtil.isEmpty(groupName)) {
            log.info("分组不存在");
            throw new ClientException("参数错误");
        }
        DomainDO domainDO = verifyDomain(domain);
        originUrls.forEach(this::verifyUrl);

        //生成短链
        List<String> shortUris = originUrls.stream()
                .map(originUrl -> shortUriUtils.generate(domainDO, originUrl))
                .toList();

        //插入数据库
        List<ShortLinkDO> shortLinks = new ArrayList<>(originUrls.size());
        for (int i = 0; i < originUrls.size(); i++) {
            long id = redissonClient.getIdGenerator(SHORT_LINK_ID_GENERATOR_KEY).nextId();
            String originUrl = originUrls.get(i);
            ShortLinkDO shortLink = ShortLinkDO.builder()
                    .id(id)
                    .userId(UserContext.getUserId())
                    .domain(domain)
                    .originUrl(originUrl)
                    .gid(gid)
                    .groupName(groupName)
                    .validDate(validDate)
                    .description(descriptions.get(i))
                    .shortUri(shortUris.get(i))
                    .favicon(UrlUtils.getFavicon(originUrl))
                    .build();
            shortLinks.add(shortLink);
        }
        boolean succeed = saveBatch(shortLinks);
        if (!succeed) {
            throw new ServiceException("批量插入失败");
        }

        //更新水平拆分表
        List<ShortLinkMapperInsertReqDTO> shortLinkMapperInserts = shortLinks.stream()
                .map(shortLink -> ShortLinkMapperInsertReqDTO.builder()
                        .id(shortLink.getId())
                        .domain(domain)
                        .shortUri(shortLink.getShortUri())
                        .originUrl(shortLink.getOriginUrl())
                        .validDate(shortLink.getValidDate())
                        .build())
                .toList();
        Result<Void> result = shortLinkMapperRemoteService.batchInsert(shortLinkMapperInserts);
        succeed = result.isSuccess();
        if (!succeed) {
            throw new RemoteException(result.getMessage());
        }

        //更新布隆过滤器
        Collection<Object> shortUriCollect = Arrays.asList(shortUris.toArray());
        RBloomFilter<Object> shortUriBloomFilter = bloomFilterUtils.create(DOMAIN_SHORT_URI_BLOOM_KEY);
        shortUriBloomFilter.add(shortUriCollect);
        shortLinks.forEach(shortLink -> {
            String redisKey = String.format(SHORT_LINK_MAPPER_KEY, domain, shortLink.getShortUri());
            stringRedisTemplate.opsForValue().set(redisKey, shortLink.getOriginUrl(), SHORT_LINK_MAPPER_CREATE_TTL, TimeUnit.MINUTES);
        });
    }

    @Override
    @Transactional
    public void updateShortLink(ShortLinkUpdateReqDTO shortLinkUpdate) {
        Long id = shortLinkUpdate.getId();
        Long gid = shortLinkUpdate.getGid();
        LocalDate validDate = shortLinkUpdate.getValidDate();
        String description = shortLinkUpdate.getDescription();
        if (ObjectUtil.isEmpty(id)) {
            log.info(String.format("id{%s} 不能为空", id));
            throw new ClientException("参数错误");
        }
        String groupName;
        if (!ObjectUtil.isEmpty(gid)) {
            groupName = groupService.getGroupNameById(gid);
            if (ObjectUtil.isEmpty(groupName)) {
                log.info("分组不存在");
                throw new ClientException("参数错误");
            }
        }

        LambdaUpdateChainWrapper<ShortLinkDO> updateWrapper = lambdaUpdate()
                .eq(ShortLinkDO::getUserId, UserContext.getUserId())
                .eq(ShortLinkDO::getDelFlag, ShortLinkDO.DEL_FLAG_FAIL)
                .eq(ShortLinkDO::getId, shortLinkUpdate.getId());
        if (!ObjectUtil.isEmpty(gid)) {
            updateWrapper = updateWrapper.set(ShortLinkDO::getGid, gid);
        }
        if (!ObjectUtil.isEmpty(validDate)) {
            updateWrapper = updateWrapper.set(ShortLinkDO::getValidDate, validDate);
        }
        if (!ObjectUtil.isEmpty(description)) {
            updateWrapper = updateWrapper.set(ShortLinkDO::getDescription, description);
        }
        boolean succeed = updateWrapper.update();
        if (!succeed) {
            throw new ServiceException("短链接更新失败");
        }

        if (!ObjectUtil.isEmpty(validDate)) {
            ShortLinkMapperUpdateReqDTO shortLinkMapperUpdate =
                    new ShortLinkMapperUpdateReqDTO(id, validDate);
            Result<Void> result = shortLinkMapperRemoteService.update(shortLinkMapperUpdate);
            boolean success = result.isSuccess();
            if (!success) {
                throw new RemoteException(result.getMessage());
            }
        }
    }

    @Override
    public void moveToRecycleBin(Long id) {
        boolean succeed = lambdaUpdate()
                .eq(ShortLinkDO::getUserId, UserContext.getUserId())
                .eq(ShortLinkDO::getDelFlag, ShortLinkDO.DEL_FLAG_FAIL)
                .eq(ShortLinkDO::getId, id)
                .set(ShortLinkDO::getDelFlag, ShortLinkDO.DEL_FLAG_SUCCESS)
                .set(ShortLinkDO::getDelTime, LocalDate.now())
                .update();
        if (!succeed) {
            throw new ServiceException("移入回收站失败!");
        }

        Result<Void> result = shortLinkMapperRemoteService.moveToRecycleBin(id);
        succeed = result.isSuccess();
        if (!succeed) {
            throw new RemoteException(result.getMessage());
        }
    }
    
    @Override
    public Map<Long, Integer> listGroupShortLinkCount(List<Long> groupIds) {
        if (ObjectUtil.isEmpty(groupIds)) {
            return new HashMap<>();
        }
        return baseMapper.listGroupShortLinkCount(UserContext.getUserId(), groupIds)
                .stream()
                .collect(Collectors.toMap(GroupShortLinkCountDTO::getGid, GroupShortLinkCountDTO::getNumber));
    }

    @Override
    public String getOriginByShortLink(String domain, String shortUri) {
        if (!ObjectUtil.isAllNotEmpty(domain, shortUri)) {
            return null;
        }
        RBloomFilter<Object> shortUriBloomFilter = bloomFilterUtils.create(String.format(DOMAIN_SHORT_URI_BLOOM_KEY, domain));
        if (!shortUriBloomFilter.contains(shortUri)) {
            return null;
        }
        String nullRedisKey = String.format(NULL_SHORT_LINK_MAPPER_KEY, domain, shortUri);
        boolean isNull = Boolean.TRUE.equals(stringRedisTemplate.hasKey(nullRedisKey));
        if (isNull) {
            return null;
        }
        String redisKey = String.format(SHORT_LINK_MAPPER_KEY, domain, shortUri);
        String originUrl = stringRedisTemplate.opsForValue().get(redisKey);
        if (!ObjectUtil.isEmpty(originUrl)) {
            return originUrl;
        }
        ShortLinkDO shortLink = lambdaQuery()
                .eq(ShortLinkDO::getShortUri, shortUri)
                .eq(ShortLinkDO::getDelFlag, ShortLinkDO.DEL_FLAG_FAIL)
                .select(ShortLinkDO::getOriginUrl)
                .one();
        if (!ObjectUtil.isEmpty(shortLink)) {
            originUrl = shortLink.getOriginUrl();
            stringRedisTemplate.opsForValue().set(redisKey, originUrl, SHORT_LINK_MAPPER_TTL, TimeUnit.MINUTES);
            return originUrl;
        } else {
            stringRedisTemplate.opsForValue().set(nullRedisKey, NULL_SHORT_LINK_MAPPER_VALUE, NULL_SHORT_LINK_MAPPER_TTL, TimeUnit.MINUTES);
            return null;
        }
    }

    /**
     * 校验域名
     */
    private DomainDO verifyDomain(String domain) {
        if (!UrlUtils.isValidDomain(domain)) {
            log.error(String.format("domain(%s) 域名格式错误", domain));
            throw new ClientException("参数错误");
        }
        DomainDO domainDO = domainService.lambdaQuery()
                .eq(DomainDO::getDomain, domain)
                .eq(DomainDO::getDelFlag, DomainDO.DEL_FLAG_FAIL)
                .one();
        if (ObjectUtil.isEmpty(domainDO)) {
            log.error("域名不存在");
            throw new ClientException("参数错误");
        }
        boolean isAuth = domainDO.verifyDomainAuth(UserContext.getUser());
        if (!isAuth) {
            log.error("没有权限");
            throw new ClientException("参数错误");
        }
        return domainDO;
    }

    /**
     * 校验url
     */
    private void verifyUrl(String originUrl) {
        String domain = UrlUtils.extractDomain(originUrl);
        if (ObjectUtil.isEmpty(domain) || !UrlUtils.isValidDomain(domain)) {
            log.info(String.format("originUrl(%s) 跳转链接填写错误", originUrl));
            throw new ClientException("参数错误");
        }

        Boolean enable = adminConfiguration.getDomainWhiteListEnable();
        if (!Boolean.TRUE.equals(enable)) {
            return;
        }

        if (!adminConfiguration.getDomainWhiteList().contains(domain)) {
            log.error(String.format("domain(%s) 域名不在白名单", domain));
            throw new ClientException("链接未被收入白名单，可能存在危险，如确定安全可联系管理员审核");
        }
    }
}